<?php
/*
 * @Author: lokei
 * @Date: 2022-08-16 13:16:33
 * @LastEditors: lokei
 * @LastEditTime: 2023-08-12 15:26:52
 * @Description: 
 */

namespace App\Http\Controllers\Pay\Weixin;

use App\Common\Tools\ResultTool;
use App\Http\Controllers\Controller;
use App\Models\Pay\ConfModel;
use App\Models\Store\StoreModel;
use Illuminate\Http\Request;

use WeChatPay\Crypto\Rsa;
use WeChatPay\Builder;
use WeChatPay\Util\PemUtil;

use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;

use WeChatPay\ClientDecoratorInterface;
use WeChatPay\Crypto\AesGcm;

class ConfController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    function resData($origin)
    {
        $data = array();
        $data['mch_name'] = $origin->mch_name;
        $data['mchid'] = $origin->mchid;
        $data['apiv2Key'] = $origin->apiv2Key;
        $data['signkey'] = $origin->signkey;
        $data['key_path'] = $origin->key_path;
        $data['cert_file'] = $origin->cert_file;
        $data['key_file'] = $origin->key_file;
        $data['wxpay_platform_certs'] = $origin->wxpay_platform_certs;
        if ($origin->profitsharing_on != null) {
            $data['profitsharing_on'] = $origin->profitsharing_on;
        }
        if ($origin->main_mchid_share != null) {
            $data['main_mchid_share'] = $origin->main_mchid_share;
        }
        return $data;
    }

    //
    public function get(Request $request)
    {
        if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
            $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
        } else {
            $conf = ConfModel::first();
            if (is_null($conf)) {
                $conf = new ConfModel();
                $conf->save();
                $conf = ConfModel::first();
            }
        }
        $res = ResultTool::success();
        $res['data'] = !is_null($conf) ? $this->resData($conf) : null;
        return $res;
    }

    public function set(Request $request)
    {
        if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
            $store = StoreModel::where('id', '=', $request->input('store_id'))->first();
            $store->mch_name = $request->input('mch_name');
            $store->mchid = $request->input('mchid');
            $store->signkey = $request->input('signkey');
            $store->key_path = $request->input('key_path');
            $store->cert_file = $request->input('cert_file');
            $store->key_file = $request->input('key_file');
            $store->save();
        } else {
            $conf = ConfModel::first();
            if (is_null($conf)) {
                $conf = new ConfModel();
            }
            $conf->mch_name = $request->input('mch_name');
            $conf->mchid = $request->input('mchid');
            $conf->apiv2Key = $request->input('apiv2Key');
            $conf->signkey = $request->input('signkey');
            $conf->key_path = $request->input('key_path');
            $conf->cert_file = $request->input('cert_file');
            $conf->key_file = $request->input('key_file');
            $conf->save();
        }
        return ResultTool::success();
    }

    public function setProfitsharing(Request $request)
    {
        if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
            $store = StoreModel::where('id', '=', $request->input('store_id'))->first();
            if (!is_null($store->wxpay_platform_certs) && $store->wxpay_platform_certs != '') {
                if ($request->input('main_mchid_share') != null) {
                    $instance = Init::getInstance($request->input('store_id'));
                    $main_mch_conf = ConfModel::first();
                    if ($request->input('profitsharing_on') == '1' && intval($request->input('main_mchid_share')) > 0) {
                        $certs_array = json_decode($store->wxpay_platform_certs);
                        $cert_serial_no = '';
                        foreach ($certs_array as $cert_info) {
                            $cert_serial_no = $cert_info->serial_no;
                            $platformCertificateFilePath = 'file://' . env('ATTACHMENT_ROOT') . 'wxpay/' . 'wechatpay_' . $cert_info->serial_no . '.pem';
                            if (config('app.proj_identity') != null && config('app.proj_identity') != '') {
                                $platformCertificateFilePath = 'file://' . env('ATTACHMENT_ROOT') . config('app.proj_identity') . '/' . 'wxpay/' . 'wechatpay_' . $cert_info->serial_no . '.pem';
                            }
                            $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
                        }
                        $encryptor = static function (string $msg) use ($platformPublicKeyInstance): string {
                            return Rsa::encrypt($msg, $platformPublicKeyInstance);
                        };

                        $instance
                            ->v3->profitsharing->receivers->add
                            ->post([
                                'json' => [
                                    'appid'        => config('app.appid'),
                                    'type' => 'MERCHANT_ID',
                                    'name' => $encryptor($main_mch_conf->mch_name),
                                    'account'        => $main_mch_conf->mchid,
                                    'relation_type'  => 'HEADQUARTER'
                                ],
                                'headers' => [
                                    'Wechatpay-Serial' => $cert_serial_no
                                ]
                            ]);
                    } else {
                        $instance
                            ->v3->profitsharing->receivers->delete
                            ->post([
                                'json' => [
                                    'appid'        => config('app.appid'),
                                    'type' => 'MERCHANT_ID',
                                    'account'        => $main_mch_conf->mchid
                                ]
                            ]);
                    }
                    $store->profitsharing_on = $request->input('profitsharing_on');
                    $store->main_mchid_share = $request->input('main_mchid_share');
                    $store->save();
                }
            }
            return ResultTool::success();
        }
        return ResultTool::fail();
    }

    /**
     * Before `verifier` executing, decrypt and put the platform certificate(s) into the `$certs` reference.
     *
     * @param string $apiv3Key
     * @param array<string,?string> $certs
     *
     * @return callable(ResponseInterface)
     */
    private function certsInjector(string $apiv3Key, array &$certs): callable
    {
        return function (ResponseInterface $response) use ($apiv3Key, &$certs): ResponseInterface {
            $body = (string) $response->getBody();
            /** @var object{data:array<object{encrypt_certificate:object{serial_no:string,nonce:string,associated_data:string}}>} $json */
            $json = \json_decode($body);
            $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
            \array_map(static function ($row) use ($apiv3Key, &$certs) {
                $cert = $row->encrypt_certificate;
                $certs[$row->serial_no] = AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data);
            }, $data);
            return $response;
        };
    }


    /**
     * After `verifier` executed, wrote the platform certificate(s) onto disk.
     *
     * @param string $outputDir
     * @param array<string,?string> $certs
     *
     * @return callable(ResponseInterface)
     */
    private static function certsRecorder(array &$certs, $store_id): callable
    {
        return static function (ResponseInterface $response) use (&$certs, $store_id): ResponseInterface {
            $outputDir = env('ATTACHMENT_ROOT');
            if (config('app.proj_identity') != null && config('app.proj_identity') != '') {
                $outputDir = $outputDir . config('app.proj_identity') . '/';
            }
            $platform_certs = array();
            $body = (string) $response->getBody();
            /** @var object{data:array<object{effective_time:string,expire_time:string:serial_no:string}>} $json */
            $json = \json_decode($body);
            $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
            \array_walk($data, static function ($row, $index, $certs) use ($outputDir, &$platform_certs) {
                $serialNo = $row->serial_no;
                if (!is_dir($outputDir . 'wxpay')) {
                    mkdir($outputDir . 'wxpay', 0700);
                }
                $outpath = $outputDir . 'wxpay/' . 'wechatpay_' . $serialNo . '.pem';
                \file_put_contents($outpath, $certs[$serialNo]);
                array_push($platform_certs, [
                    'serial_no' => $serialNo,
                    'effective_time' => $row->effective_time,
                    'expire_time' => $row->expire_time
                ]);
            }, $certs);
            if ($store_id > 0) {
                $conf = StoreModel::where('id', '=', $store_id)->first();
            } else {
                $conf = ConfModel::first();
            }
            $conf->wxpay_platform_certs = json_encode($platform_certs);
            $conf->save();
            return $response;
        };
    }

    public function gainPlatformCerts(Request $request)
    {
        if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
            $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
        } else {
            $conf = ConfModel::first();
        }

        $merchantId = $conf->mchid;

        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://' . env('ATTACHMENT_ROOT') . $conf->key_file;
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

        $merchantCertificateFilePath = 'file://' . env('ATTACHMENT_ROOT') . $conf->cert_file;
        // 「商户API证书」的「证书序列号」
        $merchantCertificateSerial = PemUtil::parseCertificateSerialNo($merchantCertificateFilePath);

        $certs = [];
        $cert_exists = false;
        if (!is_null($conf->wxpay_platform_certs) && $conf->wxpay_platform_certs != '') {
            $certs_array = json_decode($conf->wxpay_platform_certs);
            if (count($certs_array) > 0) {
                foreach ($certs_array as $cert_info) {
                    $platformCertificateFilePath = 'file://' . env('ATTACHMENT_ROOT') . 'wxpay/' . 'wechatpay_' . $cert_info->serial_no . '.pem';
                    if (config('app.proj_identity') != null && config('app.proj_identity') != '') {
                        $platformCertificateFilePath = 'file://' . env('ATTACHMENT_ROOT') . config('app.proj_identity') . '/' . 'wxpay/' . 'wechatpay_' . $cert_info->serial_no . '.pem';
                    }
                    if (file_exists($platformCertificateFilePath)) {
                        $cert_exists = true;
                        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
                        $certs[$cert_info->serial_no] = $platformPublicKeyInstance;
                    }
                }
            }
        }
        if (!$cert_exists) {
            $certs = ['any' => null];
        }

        // 构造一个 APIv3 客户端实例
        $instance = Builder::factory([
            'mchid'      => $merchantId,
            'serial'     => $merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => &$certs,
        ]);

        if (!$cert_exists) {
            /** @var \GuzzleHttp\HandlerStack $stack */
            $stack = $instance->getDriver()->select(ClientDecoratorInterface::JSON_BASED)->getConfig('handler');
            // The response middle stacks were executed one by one on `FILO` order.
            $stack->after('verifier', Middleware::mapResponse(self::certsInjector($conf->signkey, $certs)), 'injector');
            $stack->before('verifier', Middleware::mapResponse(self::certsRecorder($certs, ($request->input('store_id') != null && intval($request->input('store_id')) > 0) ? intval($request->input('store_id')) : 0)), 'recorder');

            $instance->v3->certificates->getAsync()->then(function ($request) {
                // $conf = ConfModel::first(); // 20230422获取证书
                if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
                    $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
                } else {
                    $conf = ConfModel::first();
                }
                $res = ResultTool::success();
                $res['data']['wxpay_platform_certs'] = $conf->wxpay_platform_certs;
                return $res;
            })->otherwise(function ($e) {
                app('log')->info($e);
                $res = ResultTool::fail();
                $res['errorMsg'] = $e;
                return $res;
            })->wait();
        } else {
            try {
                $resp = $instance->v3->certificates->get();
                $outputDir = env('ATTACHMENT_ROOT');
                if (config('app.proj_identity') != null && config('app.proj_identity') != '') {
                    $outputDir = $outputDir . config('app.proj_identity') . '/';
                }
                $platform_certs = array();
                $body = (string) $resp->getBody();
                /** @var object{data:array<object{effective_time:string,expire_time:string:serial_no:string}>} $json */
                $json = \json_decode($body);
                $data = \is_object($json) && isset($json->data) && \is_array($json->data) ? $json->data : [];
                $apiv3Key = $conf->signkey;
                \array_walk($data, static function ($row, $index, $certs_new) use ($apiv3Key, $outputDir, &$platform_certs) {
                    $serialNo = $row->serial_no;
                    if (!is_dir($outputDir . 'wxpay')) {
                        mkdir($outputDir . 'wxpay', 0700);
                    }
                    $outpath = $outputDir . 'wxpay/' . 'wechatpay_' . $serialNo . '.pem';
                    $cert = $row->encrypt_certificate;
                    \file_put_contents($outpath, AesGcm::decrypt($cert->ciphertext, $apiv3Key, $cert->nonce, $cert->associated_data));
                    array_push($platform_certs, [
                        'serial_no' => $serialNo,
                        'effective_time' => $row->effective_time,
                        'expire_time' => $row->expire_time
                    ]);
                }, $certs);
                // $conf = ConfModel::first(); // 20230422获取证书
                if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
                    $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
                } else {
                    $conf = ConfModel::first();
                }
                $conf->wxpay_platform_certs = json_encode($platform_certs);
                $conf->save();

                // $conf = ConfModel::first(); // 20230422获取证书
                // if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
                //     $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
                // } else {
                //     $conf = ConfModel::first();
                // }
                $res = ResultTool::success();
                $res['data']['wxpay_platform_certs'] = $conf->wxpay_platform_certs;
                return $res;
            } catch (\Exception $e) {
                app('log')->info($e);
                // 更新失败，删除无效证书
                // $conf = ConfModel::first(); // 20230422获取证书
                if ($request->input('store_id') != null && intval($request->input('store_id')) > 0) {
                    $conf = StoreModel::where('id', '=', $request->input('store_id'))->first();
                } else {
                    $conf = ConfModel::first();
                }
                $conf->wxpay_platform_certs = '';
                $conf->save();
                // 进行错误处理
                $res = ResultTool::fail();
                $res['errorMsg'] = $e->getMessage();
                return $res;
            }
        }
    }
}
